5.05. Ковариантность, контравариантность, инвариантность
Ковариантность, контравариантность, инвариантность
Ковариантность, контравариантность, инвариантность.
Это продвинутые концепции, связанные с подстановкой типов в обобщённых интерфейсах и делегатах. Начнём с примера.
У нас есть проблема - можно ли присвоить List<string> переменной типа List<object>?
List<string> strings = new List<string>();
// List<object> objects = strings; // ❌ Ошибка! Несмотря на то, что string → object
Почему? Потому что List<T> — инвариантный. Лучше использовать IEnumerable<T>.
Вариантность — это способность обобщённого типа «сохранять» или «инвертировать» отношения наследования при подстановке типов.
Допустим:
string → object // string наследует object
Теперь вопрос: если T → U, то как связаны IEnumerable<T> и IEnumerable<U>?
Ответ зависит от вариантности.
- Ковариантность (out) — сохраняет направление наследования.
T → U => I<T> → I<U>
Используется с out — тип используется только для возврата (выход).
Пример: IEnumerable<out T>
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // ✅ Работает!
Почему можно? Потому что IEnumerable<T> только возвращает элементы — безопасно.
Нельзя добавлять в ковариантный тип — только читать.
- Контравариантность (in) — инвертирует направление наследования.
T → U => I<U> → I<T>
Используется с in — тип используется только для входа (параметры).
IComparer<object> comparer = new MyObjectComparer();
IComparer<string> stringComparer = comparer; // ✅ Работает!
Почему? Потому что IComparer<string> может использовать IComparer<object> — любой string можно сравнить как object.
Нельзя возвращать такой тип — только принимать.
- Инвариантность — нет подстановки. Даже если T → U,
C<T>иC<U>— несовместимы. Пример:
List<T>, Dictionary<TKey, TValue>
List<string> strings = new List<string>();
// List<object> objects = strings; // ❌ Ошибка
Почему? Потому что можно добавить new object() в List<object>, но это сломает List<string>.
Подытожим:
| Вариантность | Ключевое слово | Направление | Где используется |
|---|---|---|---|
| Ковариантность | out | Только возвращаемые значения (например, T Get()) | IEnumerable<out T>, Func<out TResult> |
| Контрвариантность | in | Только параметры (например, void Set(T value)) | IComparer<in T>, Action<in T> |
| Инвариантность | — | Нет изменений | List<T>, Dictionary<TKey, TValue> |
Обобщённые делегаты
Делегаты тоже могут быть обобщёнными. Это мощно для абстракции и обратных вызовов.
Action<T> // void Method(T param)
Func<T, TResult> // TResult Method(T param)
Predicate<T> // bool Method(T param)
Делегаты имеют несколько применений. Некоторые из них — механизм обратного вызова, многоадресная рассылка, асинхронная обработка, а также методы абстрагирования и инкапсуляции.
Func<in T, out TResult> — контравариантен по T, ковариантен по TResult.
Это позволяет, например, использовать Func<object, string> там, где ожидается Func<string, string>.
Используются обобщения при работе с коллекциями (List<T>, HashSet<T>), при повторяющемся коде для разных типов (Box<T>, Result<T>), с алгоритмами, независимыми от типа (Max<T>, Sort<T>), при создании библиотек (Repository<T>, Serializer<T>), и в интерфейсах и абстракции (IRepository<T>, IService<T>).